feat: WASM compatibility rule checks#9587
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
3 issues found across 22 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="marimo/_lint/rules/wasm/incompatible_packages.py">
<violation number="1" location="marimo/_lint/rules/wasm/incompatible_packages.py:67">
P1: Treating `.tar.gz`/`.zip` files as WASM-compatible makes the rule miss incompatible native packages. Only compatible wheels (or known Pyodide packages) should satisfy this check.</violation>
</file>
<file name="marimo/_lint/rules/wasm/incompatible_imports.py">
<violation number="1" location="marimo/_lint/rules/wasm/incompatible_imports.py:96">
P2: The diagnostic text says these modules "will fail to import", but some flagged modules (notably `multiprocessing`/`subprocess`) are importable in Pyodide and fail at runtime usage instead.</violation>
</file>
Architecture diagram
sequenceDiagram
participant CLI as CLI (marimo check)
participant Export as html_wasm Export
participant Selector as rule_selector.resolve_rules()
participant Init as rules/__init__.py
participant Engine as RuleEngine
participant Lint as Linter
participant MW001 as MW001 IncompatibleImportsRule
participant MW002 as MW002 UnsafeSystemCallsRule
participant MW003 as MW003 IncompatiblePackagesRule
participant PyPI as PyPI JSON API
participant Pyodide as pyodide-lock.json
Note over CLI,MW003: NEW: WASM rule category (MW) – off by default
CLI->>Engine: create_default()
Engine->>Selector: resolve_rules(config)
alt No --select provided
Selector->>Init: Read DEFAULT_RULE_CODES
Init-->>Selector: Breaking + Runtime + Formatting only
Selector-->>Engine: Default rule instances (no MW)
else --select MW or --select ALL
Selector->>Init: Read RULE_CODES (includes WASM)
Init-->>Selector: All rules including MW001-003
Selector-->>Engine: Selected rule instances
end
Engine-->>CLI: RuleEngine ready
alt WASM rules selected
CLI->>Lint: lint(notebook)
Lint->>MW001: check() – scan imports
MW001->>MW001: Match cell imports against INCOMPATIBLE_MODULES
alt Found incompatible import
MW001-->>Lint: Diagnostic MW001
end
Lint->>MW002: check() – scan AST for unsafe calls
MW002->>MW002: Visit Call nodes for os.*, signal.*, breakpoint()
alt Found unsafe call
MW002-->>Lint: Diagnostic MW002
end
Lint->>MW003: check() – resolve dependency tree
MW003->>Pyodide: fetch_pyodide_package_versions()
Pyodide-->>MW003: Set of available packages
MW003->>MW003: Extract PEP 723 dependencies from notebook
MW003->>MW003: Walk transitive deps via importlib.metadata
loop Each unique package
alt Package in Pyodide set
Note over MW003: Package available in Pyodide
else Check PyPI
MW003->>PyPI: GET /pypi/{name}/json
PyPI-->>MW003: Wheel metadata
alt Has py3-none-any or emscripten wheel
Note over MW003: Compatible wheel found
else Only native wheels
MW003->>MW003: Add to incompatible list
end
end
end
alt Incompatible packages found
MW003-->>Lint: Diagnostic MW003
end
Lint-->>CLI: All diagnostics
end
Note over Export,MW003: CHANGED: html_wasm export runs MW lint pass
Export->>MW003: run_check(select=["MW"])
MW003-->>Export: WASM compatibility diagnostics (stderr)
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| ): | ||
| return True | ||
| # Source distributions can be built as pure-python by micropip. | ||
| if filename.endswith((".tar.gz", ".zip")): |
There was a problem hiding this comment.
P1: Treating .tar.gz/.zip files as WASM-compatible makes the rule miss incompatible native packages. Only compatible wheels (or known Pyodide packages) should satisfy this check.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At marimo/_lint/rules/wasm/incompatible_packages.py, line 67:
<comment>Treating `.tar.gz`/`.zip` files as WASM-compatible makes the rule miss incompatible native packages. Only compatible wheels (or known Pyodide packages) should satisfy this check.</comment>
<file context>
@@ -0,0 +1,224 @@
+ ):
+ return True
+ # Source distributions can be built as pure-python by micropip.
+ if filename.endswith((".tar.gz", ".zip")):
+ return True
+
</file context>
There was a problem hiding this comment.
No, pypi doesn't distribute non-wheels and finding a value with this raw extension is probably an indication it was intentional
There was a problem hiding this comment.
Thanks for the clarification. If you can share a reference or an example, that would help ensure we handle this case correctly.
| Diagnostic( | ||
| message=( | ||
| f"Module '{top_level}' is not available in " | ||
| "WASM/Pyodide and will fail to import." |
There was a problem hiding this comment.
P2: The diagnostic text says these modules "will fail to import", but some flagged modules (notably multiprocessing/subprocess) are importable in Pyodide and fail at runtime usage instead.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At marimo/_lint/rules/wasm/incompatible_imports.py, line 96:
<comment>The diagnostic text says these modules "will fail to import", but some flagged modules (notably `multiprocessing`/`subprocess`) are importable in Pyodide and fail at runtime usage instead.</comment>
<file context>
@@ -0,0 +1,102 @@
+ Diagnostic(
+ message=(
+ f"Module '{top_level}' is not available in "
+ "WASM/Pyodide and will fail to import."
+ ),
+ line=line,
</file context>
There was a problem hiding this comment.
Thanks for the feedback!
There was a problem hiding this comment.
Pull request overview
Adds a new opt-in “WASM compatibility” lint category (MW001–MW003) so marimo can detect common Pyodide/WASM incompatibilities, and wires these rules into rule selection, docs, and the WASM export path.
Changes:
- Introduces
Severity.WASMand a newmarimo._lint.rules.wasmrule set (MW001 incompatible imports, MW002 unsafe system calls, MW003 incompatible packages). - Splits rule registration into
DEFAULT_RULE_CODES(enabled by default) vsRULE_CODES(includes opt-in categories like WASM), updating selector/engine/tests accordingly. - Updates docs + mkdocs nav, and runs a WASM lint pass during
marimo export html-wasm --execute.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/_lint/test_wasm_rules.py | Adds tests for MW rules being off-by-default and for MW001/MW002 behavior. |
| tests/_lint/test_rule_selector.py | Updates selector tests for default vs ALL rules; adds MW selection tests. |
| tests/_lint/test_lint_config_integration.py | Updates integration assertions to use DEFAULT_RULE_CODES for default engine/linter creation. |
| tests/_lint/test_files/wasm_incompatible.py | Adds a fixture notebook containing WASM-incompatible imports/calls. |
| scripts/generate_lint_docs.py | Extends lint-doc generation to include WASM severity and MW code validation. |
| mkdocs.yml | Adds navigation entries for new MW rule docs pages. |
| marimo/_lint/rules/wasm/unsafe_system_calls.py | Implements MW002 to flag unsupported runtime calls (e.g., os.system, breakpoint). |
| marimo/_lint/rules/wasm/incompatible_packages.py | Implements MW003 to flag dependency-tree packages lacking WASM-compatible artifacts. |
| marimo/_lint/rules/wasm/incompatible_imports.py | Implements MW001 to flag imports of stdlib modules unavailable in Pyodide. |
| marimo/_lint/rules/wasm/init.py | Registers WASM rules and exports WASM_RULE_CODES. |
| marimo/_lint/rules/init.py | Introduces DEFAULT_RULE_CODES and expands RULE_CODES to include opt-in WASM rules. |
| marimo/_lint/rule_selector.py | Changes default behavior to select DEFAULT_RULE_CODES unless select is provided. |
| marimo/_lint/rule_engine.py | Uses DEFAULT_RULE_CODES when creating the default rule engine without config. |
| marimo/_lint/diagnostic.py | Adds Severity.WASM. |
| marimo/_lint/context.py | Adds WASM severity to diagnostic priority mapping. |
| marimo/_lint/init.py | Adds WASM severity ordering for filtering/aggregation. |
| marimo/_cli/export/commands.py | Runs MW lint selection during html-wasm --execute. |
| docs/guides/lint_rules/rules/unsafe_system_call.md | Adds MW002 documentation page. |
| docs/guides/lint_rules/rules/incompatible_package.md | Adds MW003 documentation page. |
| docs/guides/lint_rules/rules/incompatible_import.md | Adds MW001 documentation page. |
| docs/guides/lint_rules/index.md | Documents the new WASM rule category and lists MW rules. |
| development_docs/adding_lint_rules.md | Documents WASM severity and the default vs opt-in rule registration model. |
| # Step 1: base set — use only default rules unless explicitly selected | ||
| if select: | ||
| codes = {c for c in codes if _matches_any_prefix(c, select)} | ||
| codes = {c for c in all_rules if _matches_any_prefix(c, select)} | ||
| else: | ||
| from marimo._lint.rules import DEFAULT_RULE_CODES | ||
|
|
||
| codes = set(DEFAULT_RULE_CODES.keys()) & set(all_rules.keys()) |
| @functools.cache | ||
| def _has_wasm_compatible_wheel(package_name: str) -> bool: | ||
| """Check PyPI for a pure-python or emscripten wheel. | ||
|
|
||
| Returns True if micropip can install this package (has a | ||
| py3-none-any wheel, a py2.py3-none-any wheel, or an | ||
| emscripten/wasm32 wheel). Returns True on network failure | ||
| (fail open). Cached so a single export with N transitive deps | ||
| hits PyPI at most once per unique package name. | ||
| """ | ||
| url = f"https://pypi.org/pypi/{package_name}/json" | ||
| try: | ||
| with urllib.request.urlopen(url, timeout=10) as resp: | ||
| data = json.loads(resp.read()) | ||
| except Exception: | ||
| return True # Can't check — assume compatible. | ||
|
|
There was a problem hiding this comment.
It is gated, it's opt in with --select. But I do think we should try to respect the index
| def test_select_wasm_rules(self): | ||
| rules = resolve_rules({"select": ["MW"]}) | ||
| assert all(r.code.startswith("MW") for r in rules) | ||
| assert len(rules) == 3 | ||
|
|
| # Copyright 2026 Marimo. All rights reserved. | ||
| """Tests for WASM compatibility lint rules (MW001, MW002, MW003).""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from marimo._ast.parse import parse_notebook |
| urls = data.get("urls", []) | ||
| if not urls: | ||
| return True # No files — likely a namespace package. | ||
|
|
||
| for file_info in urls: | ||
| filename = file_info.get("filename", "") | ||
| if filename.endswith(".whl"): | ||
| if ( | ||
| "none-any" in filename | ||
| or "emscripten" in filename | ||
| or "wasm" in filename | ||
| ): | ||
| return True | ||
| # Source distributions can be built as pure-python by micropip. |
📝 Summary
Adds new class of lint rules
MWfor marimo wasm compatibility checks, codifying https://github.com/marimo-team/skills/tree/main/skills/wasm-compatibilityNote: these skills are opt-in (
marimo check --select MW) and are somewhat expensive (MW003: incompatible-packagein particular requires network access to determine dependencies).The new rules are:
MW001: incompatible-importmultiprocessing, andsubprocessMW002: unsafe-system-callos.system)MW003: incompatible-package